Electric Lunatic

www.ElectricLunatic.net

Image Water Marking Technique

Using .NET and C#



Introduction

I wanted to add a watermark to the images on my web site to discourage AI entities from scraping images and incorporate them into an AI generated web page or video (I have heard this is a problem). I didn't want to spend money on purchasing software and I don't entirely trust online watermarking services with my intellectual property. Note that this example of watermarking software only uses text as a watermark. It is possible to use an image as a watermark, but not in this example.

I'm a software developer, why don't I write my own watermarking software? So this is what I did... I wanted to share the technique I developed with other people and they may use it for personal purposes. Any code listed here in this artical may not be used verbatim commercially.

I chose the C# programming language on the .NET platform as it is my development platform of choice for modern Windows applications. VB.NET will work just as well. This artical mostly explains the technique involved along with C# code samples. A code listing for a complete application is not given.

This artical was written presuming that the reader has some knowledge of and experience with the .NET platform and the C# or VB programming languages. I use the Pascal naming convention for variable and method names. Some readers may not care for this but that's the way I do it. I think naming conventions are trivial anyway.


Discussion

The technique described here uses the .NET Drawing and Drawing.Imaging namespaces and these must be included in the source file with the "using" directive as shown :

                    using Drawing;
                    using Drawing.Imaging;
                

An image is to be loaded from an image file. The .NET Image, ImageFormat, Bitmap, and Graphics classes are used as well as the enumeration PixelFormat.

Indexed Pixel Formats

One important thing to note is that an instance of a Graphics object cannot be created from an indexed pixel format. An indexed pixel format does not store a color value for each pixel but rather a reference index to a color. (This would found in GIF and 1 bit BMP file formats.) For this reason, if the file being watermarked is of an indexed pixel format, the watermarked image produced must be of an alternate pixel format that is not an indexed pixel format. Also, in the case of a GIF image being watermarked, an alternate image format has to be used as GIF images only store indexed pixel colors.

Logic Flow

So the reader doesn't have to look at a flow chart (and I don't feel like drawing one anyway), the logic flow is shown as a simple sequential list :

  1. Create an Image object from an image file.
  2. Set watermarked image and pixel formats to the formats of the original image.
  3. If the original pixel format is an indexed pixel format, set watermarked image and pixel formats to the alternate formats.
  4. Create a Bitmap object of the same dimensions as the original image using the watermarked image pixel format. This will contain the original image.
  5. Create a Graphics object from the Bitmap object created in the previous step.
  6. Using the Graphics object created in the previous step, draw the bit map created in step 4 to the Image object created in step 1.
  7. Dispose of the Image object created in step 1. (The image is still in the Graphics object.)
  8. Call the method DrawWaterMark to draw the watermark on the image.
  9. Dispose of the Graphics object created in step 5.
  10. Create a Bitmap object of the same dimensions as the original image using the watermarked image pixel format. This will contain the watermarked image.
  11. Create a Graphics object from the Bitmap object created in the previous step.
  12. Using the Graphics object created in the previous step, draw the bit map of the watermarked image created in step 10.
  13. Create an Image object from the handle of the Bitmap object of the watermarked image created in step 10.
  14. Save the Image object created in the previous step, to the image format for the watermarked image to a file.

This is fiddly business but this is how I made it work with my understanding of the the .NET Drawing namespace.

Code Sample

So lets get into some code samples. The code is heavily commented to document what is happening in the software. Usually this code sample would be contained in it's own method, but it doesn't have to be (depending on how it's used) so the code is shown as a "disembodied entity", for lack of better words. This code snippet has been compiled and run inside of an application that I use. It does work correctly.

                    // Objects declared first for clarity...

                    // The objects are initialized to null because the compiler squawks about referencing 
                    // unassigned objects in the finally block. Unassigned does not mean null.

                    Image       ImageObj     = null; // Image object to contain the original and watermarked image.
                    ImageFormat ImgFmt       = null; // ImageFormat object for the original and watermarked image.     
                    ImageFormat AltImgFmt    = null; // Alternate ImageFormat object (may be a method parameter).
                    Bitmap      OrigBitMap   = null; // Bitmap object to contain the original image.
                    Bitmap      MarkedBitMap = null; // Bitmap object to contain the watermarked image.
                    Graphics    GraphicsObj  = null; // Graphics object used for drawing the image.
                    
                    // Variables declared first for clarity...
                    
                    int         ImgWidth;       // Image width, in pixels.
                    int         ImgHeight;      // Image height, in pixels.
                    string      OrigFilePath;   // Original image file pathname (may be a method parameter).
                    string      MarkedFilePath; // Watermarked image file pathname (could be a method parameter).
                    string      MarkText1;      // Watermark text, line 1 (may be a method parameter).
                    string      MarkText2;      // Watermark text, line 2 (may be a method parameter).
                    PixelFormat PixFmt;         // Pixel format for original and watermarked image.
                    PixelFormat AltPixFmt;      // Alternate pixel format (may be a method parameter).

                    // Of course wrap everything in a try-catch-finally block...
                    try
                    {
                        // Instantiate ImageObj from the original image from a file.
                        ImageObj = Image.FromFile(OrigFilePath);

                        // Store the image dimensions because ImageObj containing the original
                        // image will be disposed of and used again for the watermarked image.
                        ImgWidth  = ImageObj.Width;
                        ImgHeight = ImageObj.Height;

                        // Store the pixel and image formats of the original image.
                        PixelFormat PixFmt = ImageObj.PixelFormat;
                        ImageFormat ImgFmt = ImageObj.RawFormat;

                        // If the original image uses an indexed pixel format, we have to use an alternate 
                        // pixel and image format. The alternate formats are expected to be supplied by the 
                        // user as a method parameter or could be hard coded.
                        if((ImageObj.PixelFormat == PixelFormat.Format1bppIndexed) ||
                           (ImageObj.PixelFormat == PixelFormat.Format4bppIndexed) ||
                           (ImageObj.PixelFormat == PixelFormat.Format8bppIndexed))
                        {
                            PixFmt = AltPixFmt;
                            ImgFmt = AltImgFmt;
                        }
                        
                        // Now create new Bitmap object the same dimensions as the original with the 
                        // proper pixel format. The Bitmap object is empty at this time.
                        OrigBitMap = new Bitmap(ImgWidth, ImgHeight, PixFmt);

                        // Create a Graphics object from the new Bitmap object. 
                        GraphicsObj = Graphics.FromImage(OrigBitMap);

                        // Draw the original image in GraphicsObj using the proper pixel format.
                        GraphicsObj.DrawImage(ImageObj, new Rectangle(0, 0, ImgWidth, ImgHeight));

                        // Dispose of ImageObj containing the original image as we no longer need it.
                        // It is set to null so that in the case that an exception is thrown, it isn't 
                        // disposed twice in the finally block.
                        ImageObj.Dispose();
                        ImageObj = null;

                        // Call the method DrawWaterMark to draw the watermark on the original image 
                        // using GraphicsObj. 
                        DrawWaterMark(GraphicsObj, 
                                      ImgWidth, ImgHeight,
                                      FontSize, 
                                      MarkText1, MarkText2, 
                                      MarkDiag);

                        // Dispose of this instrance of GraphicsObj object after it has been used
                        // to draw the watermark.
                        GraphicsObj.Dispose();
                        GraphicsObj = null;

                        // Create a new bit map object for the watermarked bit map...
                        MarkedBitMap = new Bitmap(ImgWidth, ImgHeight, PixFmt);

                        // Create a new instance of GraphicsObj using MarkedBitMap...
                        GraphicsObj = Graphics.FromImage(MarkedBitMap);

                        // Draw the watermarked image (which is in OrigBitMap and already watermarked)
                        // into MarkedBitMap. This seems redundant. The reason for the transfer of the 
                        // image from OrigBitMap to MarkedBitMap is to re-draw the image in the alternate
                        // pixel format, if in fact the alternate pixel fortmat is used. It the pixel 
                        // format is still the original format, no harm is done.
                        GraphicsObj.DrawImage(OrigBitMap, 
                                              new Rectangle(0, 0, ImgWidth, ImgHeight)); 

                        // Get a handle to the final watermarked image...
                        ImageObj = Image.FromHbitmap(MarkedBitMap.GetHbitmap());
                   
                        // Now save the watermarked image.
                        ImageObj.Save(MarkedFilePath, ImgFmt); 
                    } 
                    catch(Exception Exc)
                    {
                        // Handle exception here...
                    }
                    finally
                    {
                        // Dispose of all disposable objects before exiting method...
                        if(GraphicsObj  != null) GraphicsObj.Dispose();
                        if(ImageObj     != null) ImageObj.Dispose();
                        if(OrigBitMap   != null) OrigBitMap.Dispose();
                        if(MarkedBitMap != null) MarkedBitMap.Dispose();
                    }
                

Drawing The Watermark

The code for drawing the watermark on the image is contained in the method DrawWaterMark, which calls the methods DrawHorizWaterMark and DrawDiagWaterMark. This software allows for the watermark to be two stacked lines of text, but may be a single line of text. The watermark may be drawn horizontally across the center of the image or diagonally across the center of the image (upper left to lower right). The watermark text is drawn in a faded gray tone as to be noticed but not dominate the image. Of course, what is noticeable but not dominating is a matter of personal opinion.

Code Sample

The code for the method DrawWaterMark is shown below :

                    private void DrawWaterMark(Graphics GraphicsObj,    // Graphics object containing the handle of the image.
                                               int      ImgWidth,       // Image height and width in pixels.
                                               int      ImgHeight,
                                               int      FontSize,       // Font size.
                                               string   WaterMarkText1, // Waterwark text line 1. May not be null.
                                               string   WaterMarkText2, // Watermark text line 2. May be null or empty.
                                               bool     Diagonal)       // True if watermark is to be drawn on the diagonal.
                    {
                        string FontName     = "Arial";  // Name of font used for watermark text (may be a method parameter).
                        int    Transparency = 100;      // Transparency of watermark, 100% (may be a method parameter).
                        int    Red          = 190;      // RGB color values for light gray (may be a method parameter).
                        int    Blue         = 190; 
                        int    Green        = 190; 

                        // Create a Font object for the font name and size given.
                        Font TextFont = new Font(FontName, FontSize,
                                                 FontStyle.Bold, 
                                                 GraphicsUnit.Pixel);

                        // Create a Color structure for watermark color and transparency.
                        Color TextColor = Color.FromArgb(Transparency, 
                                                         Red, Blue, Green);

                        // Create a SolidBrush object to draw the watermark.
                        SolidBrush TextBrush = new SolidBrush(TextColor);

                        // Set the graphics X, Y origin (0, 0) to the center of the image. All
                        // coordinates will referenced from this point. That's why the X, y
                        // position(s) of the water marked text are subtracted from zero.
                        GraphicsObj.TranslateTransform(ImgWidth / 2, ImgHeight / 2);

                        if(Diagonal)                        
                            DrawDiagWaterMark(GraphicsObj,          // Draw a diagonal watermark...
                                              ImgWidth, ImgHeight,
                                              TextFont, TextColor, TextBrush, 
                                              WaterMarkText1, WaterMarkText2);
                        else
                            DrawHorizWaterMark(GraphicsObj,         // Draw a horizontal watermark...
                                               TextFont, TextColor, TextBrush, 
                                               WaterMarkText1, WaterMarkText2);
                    }
                

There is no exception handling in DrawWaterMark as any exceptions will be handled in the calling code.

Drawing A Horizontal Watermark

Two diagrams to show how the position of the watermark text is determined is shown below. The first for a single line watermark and the second for a two line watermark. The coordinate origin (0, 0) is set to the center of the image (this is done in the method DrawWaterMark) and this is why the text metrics are subtracted from zero.

For a single line of text, the position of the upper left corner of the text is determined by dividing the height and width of the text and subtracting these values from zero.

For two lines of watermark text, the position of the left side of each of the lines of text is determined as it is for a single line of text. However, the first line will be placed above the horizontal center line of the image and the second line below it.

Code Sample

The code for the method DrawHorizWaterMark is shown below :

                    private void DrawHorizWaterMark(Graphics   GraphicsObj,    // Graphics object containing the handle of the image.
                                                    Font       TextFont,       // Font, Color and Brush objects...
                                                    Color      TextColor,
                                                    SolidBrush TextBrush,
                                                    string     WaterMarkText1, // Waterwark text line 1. May not be null.
                                                    string     WaterMarkText2) // Watermark text line 2. May be null or empty.
                    {
                        int    PosX;     // Horizontal position of the upper left corner of text in pixels.
                        int    PosY;     // Vertical position of the upper left corner of text in pixels.
                        SizeF  TextSize; // SizeF structure for watermark text.
                        Point  TextPos;  // Point structure for upper left corner of text.

                        // If WaterMarkText2 is null or empty, this watermark is composed of a single line
                        // of text (WaterMarkText1). The single line of text is drawn centered in the image.
                        if(string.IsNullOrEmpty(WaterMarkText2))
                        {
                            // WaterMarkText1 cannot be null and shouldn't be empty. Caluculate the size of
                            // WaterMarkText1 as it would appear on the image.
                            TextSize = GraphicsObj.MeasureString(WaterMarkText1, TextFont);

                            // Calculate X and Y positions for the upper left corner of WaterMarkText1.
                            // Separate variables (PosX and PosY) are being used for clarity.
                            PosX = (int)(0.0 - TextSize.Width / 2.0);
                            PosY = (int)(0.0 - TextSize.Height / 2.0);

                            // Assign the position values to the structure TextPos.
                            TextPos = new Point(PosX, PosY);

                            // Using GraphicsObj, draw WaterMarkText1 on image.
                            GraphicsObj.DrawString(WaterMarkText1, 
                                                   TextFont, 
                                                   TextBrush, 
                                                   TextPos);
                        }
                        // If WaterMarkText2 is not null or empty, this watermark is composed of two lines
                        // of watermark text. Both lines of text are centered in the image horizontally.
                        // The first line of text (WaterMarkText1) is drawn above the vertical center line of
                        // the image and the second line of text (WaterMarkText2) is drawn below the vertical
                        // center line of the image.
                        else
                        {
                            // Get the size of WaterMarkText1 as it would appear on the image.
                            TextSize = GraphicsObj.MeasureString(WaterMarkText1, TextFont);

                            // Calculate X and Y positions for the upper left corner of WaterMarkText1.
                            // Separate variables (PosX and PosY) are being used for clarity.
                            PosX = (int)(0.0 - TextSize.Width / 2.0);
                            PosY = (int)(0.0 - TextSize.Height); 

                            // Assign the position values to the structure TextPos.
                            TextPos = new Point(PosX, PosY);

                            // Using GraphicsObj, draw WaterMarkText1 on image.
                            GraphicsObj.DrawString(WaterMarkText1, 
                                                   TextFont, 
                                                   TextBrush, 
                                                   TextPos);

                            // Get the size of WaterMarkText2 as it would appear on the image.
                            TextSize = GraphicsObj.MeasureString(WaterMarkText2, TextFont);

                            // Calculate X and Y positions for the upper left corner of WaterMarkText2.
                            PosX = (int)(0.0 - TextSize.Width / 2);
                            PosY = 0; 

                            // Assign X and Y positions to the structure TextPos.
                            TextPos = new Point(PosX, PosY); 

                            // Using GraphicsObj, draw WaterMarkText2 on image.
                            GraphicsObj.DrawString(WaterMarkText2, 
                                                   TextFont, 
                                                   TextBrush, 
                                                   TextPos);
                        }
                    }
                

Drawing A Diagonal Watermark

A diagram showing how the angle of rotation for a diagonal watermark is shown below :

The watermark text is rotated around the center of the image (origin at 0, 0). The angle of rotation is calculated by taking the arctangent of the ratio of height and width of the image being watermarked. The rotation is done with Graphics.RotateTransform. The watermark text is drawn just as it is for a horizontal watermark.

Code Sample

The code for the method DrawDiagWaterMark is shown below :

                    private void DrawDiagWaterMark(Graphics   GraphicsObj,    // Graphics object containing the handle of the image.
                                                   int        ImgWidth,       // Image height and width in pixels.
                                                   int        ImgHeight,
                                                   Font       TextFont,       // Font, Color and Brush objects...
                                                   Color      TextColor,
                                                   SolidBrush TextBrush,
                                                   string     WaterMarkText1, // Waterwark text line 1. May not be null.
                                                   string     WaterMarkText2) // Watermark text line 2. May be null or empty.
                    {
                        // Calculate angle of the line spanning the upper left corner to the lower right
                        // corner of the image. The angle is expressed in radians.
                        float TextAngle = (float)Math.Atan(((double)ImgHeight / (double)ImgWidth));

                        // Convert from radians to degrees.
                        TextAngle = (float)(180.0 / Math.PI * TextAngle);

                        // Rotate the axis in which the text is drawn by the angle TextAngle. The text
                        // is rotated at the origin (0, 0 center of the image).
                        GraphicsObj.RotateTransform(TextAngle);

                        // Simple call DrawHorizWaterMark. The horizontal center line has been rotated. 
                        DrawHorizWaterMark(GraphicsObj,
                                           TextFont, TextColor, TextBrush,
                                           WaterMarkText1,
                                           WaterMarkText2);
                    }
                

Closing Notes

If a watermarked image is to be rendered in a web page using the HTML graphics element img with height and width parameters, the height and width parameters should be of the same or very similar proportions of the original image. Otherwise the watermark text will appear slanted either forward or backward.

An image may be used as a watermark as well. The method Graphics.DrawImage could probably be used instead of Graphics.DrawString, but I haven't tried it.




Copyright 2026, Jon T. Qualey. All Rights Reserved


Back To Software And Concepts